#region File information

/**
 * File: World.cs
 * Date: 27-03-2008
 * Auth: Jan-Jaap Severs
 * 
 * Desc: The game world, manages entities such as ships and projectiles, and renders the scenery.
 */

#endregion

#region Using statements

using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Xna.Framework;
using Gremlin.Components;
using Squadron.Networking;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Content;
using Gremlin;

#endregion

namespace Squadron
{
    /// <summary>
    /// The game world, manages entities such as ships and projectiles, and renders the scenery.
    /// </summary>
    class World
    {
        #region Member variables

        /// The local player's ship.
        Ship localShip = null;

        /// The remote players' ships.
        List<Ship> ships = new List<Ship>();

        /// The projectiles that have been fired into the world.
        List<Projectile> projectiles = new List<Projectile>();

        UInt16 localID = 0;

        /// A reference to the main application class.
        Game _game;

        SkyBox skyBox;
        ParticleSystem smokePlumeParticles;
        
        #endregion

        /// <summary>
        /// Creates a new World.
        /// </summary>
        /// <param name="game">Reference to the host application class.</param>
        public World(Game game)
        {
            _game = game;

            // TEMP:
            Grid grid = new Grid(_game);
            grid.GridScale = 64;
            grid.GridSize = 32;
            _game.Components.Add(grid);

            // Create a particle system for testing.
            smokePlumeParticles = new ParticleSystem(_game, _game.Content, "SmokePlumeSettings");
            
            _game.Components.Add(smokePlumeParticles);
        }

        public void Initialize(ContentManager contentManager)
        {
            skyBox = new SkyBox(_game, contentManager);
            skyBox.Initialize(new Vector3(0, 0, 0), new Vector3(0, 0, 1), _game.GraphicsDevice.Viewport);
            //_game.Components.Add(skyBox);
        }

        public void Update(GameTime gameTime)
        {
            List<int> deadProjectiles = new List<int>();
            foreach (Projectile proj in projectiles)
            {
                proj.Update(gameTime);
                if (proj.IsDead)
                    deadProjectiles.Add(projectiles.IndexOf(proj));
            }

            UpdateShips(gameTime);

            foreach (int i in deadProjectiles)
                projectiles.RemoveAt(i);

            Camera cam = ((CameraProvider)_game.Services.GetService(typeof(ICameraService))).Camera;
            if (cam != null)
            {
                skyBox.Update(gameTime, cam.Position, cam.ViewingDirection);

                smokePlumeParticles.AddParticle(Vector3.Zero, Vector3.Zero);
                smokePlumeParticles.SetCamera(cam.ViewMatrix, cam.ProjMatrix);
                smokePlumeParticles.Position = localShip.Position;
                smokePlumeParticles.Gravity = -localShip.Direction;
            }
        }

        /// <summary>
        /// Updates the ship objects.
        /// </summary>
        /// <param name="gameTime">Snapshot of the current timing values.</param>
        public void UpdateShips(GameTime gameTime)
        {   
            // Update the ships.
            if (localShip != null)
            {
                localShip.Update(gameTime);

                foreach (Projectile proj in projectiles)
                    if (localShip.Hits(proj))
                        localShip.TakeDamage(proj);
            }

            // Check if any of the ships are hit by a projectile.
            foreach (Ship ship in ships)
            {
                ship.Update(gameTime);

                foreach (Projectile proj in projectiles)
                    if (ship.Hits(proj))
                        ship.TakeDamage(proj);
            }
        }

        /// <summary>
        /// Renders the game world.
        /// </summary>
        public void Draw(GameTime gameTime)
        {
            skyBox.Draw(gameTime);

            if (localShip != null)
                localShip.Draw();

            foreach (Ship ship in ships)
                ship.Draw();

            foreach (Projectile proj in projectiles)
                proj.Draw();
        }

        /// <summary>
        /// A client has joined the game, so add a ship to represent it.
        /// </summary>
        /// <param name="gamerIndex">Unique index of the client in the networksession.</param>
        public void AddRemoteShip(UInt16 gamerIndex)
        {
            // Create a ship instance for every competing gamer.
            Ship ship = new Ship(_game, _game.Content, "Models\\MessTest");
           
            // Give the ship a unique ID.
            ship.ID = gamerIndex;
            ships.Add(ship);
        }

        /// <summary>
        /// Create a packet containing the current input state of the local client.
        /// </summary>
        /// <returns>ClientPacket containing the input state.</returns>
        public ClientPacket GetClientPacket()
        {
            if (localShip == null)
                return null;

            // Collect input from keyboard or controller.
            ClientPacket clientPacket = ReadShipInput(localShip.ID);

            return clientPacket;
        }

        /// <summary>
        /// Creates a snapshot of objects' PRS state.
        /// </summary>
        /// <returns>A WorldData packet.</returns>
        public WorldData GetWorldData()
        {
            // Create a WorldData packet and send that across the network.
            List<Vector3> positions = new List<Vector3>();
            List<Quaternion> rotations = new List<Quaternion>();
            List<UInt16> ids = new List<UInt16>();

            int shipcount = 0;

            // Loop over all the remote players in the session.
            foreach (Ship ship in ships)
            {                
                // Write the ship state into the output network packet.                               
                positions.Add(ship.Position);
                rotations.Add(ship.Rotation);
                ids.Add(ship.ID);

                shipcount++;
            }

            // Update the local ship.                
            if (localShip != null)
            {                
                positions.Add(localShip.Position);
                rotations.Add(localShip.Rotation);
                ids.Add(localID);

                shipcount++;
            }

            WorldData worldDataPacket = new WorldData((UInt16)(shipcount), positions, rotations, ids);

            return worldDataPacket;
        }

        /// <summary>
        /// A WorldData packet has been sent by the host. Update the local world accordingly.
        /// </summary>
        /// <param name="packet">The WorldData packet containing the snapshot.</param>
        public void ProcessWorldDataPacket(WorldData packet)
        {
            for (int i = 0; i < packet.NumShips; i++)
            {
                UInt16 id = packet.IDs[i];
                Vector3 pos = packet.Positions[i];
                Quaternion rot = packet.Rotations[i];
                bool found = false;

                if (localShip.ID == id)
                {
                    localShip.Position = pos;
                    localShip.Rotation = rot;
                    found = true;

                    continue;
                }

                foreach (Ship ship in ships)
                {
                    if (ship.ID == id)
                    {
                        ship.Position = pos;
                        ship.Rotation = rot;
                        found = true;

                        continue;
                    }
                }

                if (found == false)
                {
                    // Add a new ship to the collection.
                    Ship ship = new Ship(_game, _game.Content, "Models\\MessTest");
                    ship.ID = id;
                    ship.Position = pos;
                    ship.Rotation = rot;

                    ships.Add(ship);
                }
            }
        }

        /// <summary>
        /// Updates a client-controlled ship with the latest user-input.
        /// </summary>
        /// <param name="packet">The ClientPacket that's been received by the server.</param>
        public void ProcessClientInputPacket(ClientPacket packet)
        {
            // Find the ship with the appropriate ID.
            for (int i = 0; i < ships.Count; i++)
            {
                if (ships[i].ID == packet.ID)
                {
                    ships[i].UpdatePlayerInput(packet);

                    if (packet.FireGuns)
                    {
                        Projectile p = ships[i].Fire();
                        if(p != null)
                            projectiles.Add(p);
                    }

                    return;
                }
            }

            // We were unable to find the ID amongst the ships.
            // TODO: handle this situation.
            throw new Exception("Unable to locate ship with ID: " + packet.ID.ToString());
        }

        /// <summary>
        /// The host informs the local client of its unique ID in the networksession.
        /// </summary>
        /// <param name="packet">The ClientJoinedPacket containing the unique ID.</param>
        public void ProcessClientJoinedPacket(ClientJoinedPacket packet)
        {
            this.localID = packet.ID;

            localShip = new Ship(_game, _game.Content, "Models\\MessTest");
            localShip.ID = localID;

            SpringCamera cam = new SpringCamera(_game, localShip);
            cam.Initialize();
            _game.Components.Add(cam);
        } 

        /// <summary>
        /// The host doesn't have to send its input across the network, so we can handle it locally.
        /// </summary>
        public void UpdateHostLocalShip()
        {
            if (localShip == null)
                return;

            // Collect input from keyboard or controller.
            ClientPacket clientPacket = ReadShipInput(localShip.ID);

            localShip.UpdatePlayerInput(clientPacket);

            if (clientPacket.FireGuns)
            {
                Projectile p = localShip.Fire();
                if(p != null)
                    projectiles.Add(p);
            }
        }

        /// <summary>
        /// Reads input from a local controller or the keyboard.
        /// <todo>Refactor to a ship-input class.</todo>
        /// </summary>
        ClientPacket ReadShipInput(UInt16 id)
        {
            float deltaX = 0.0f, deltaY = 0.0f;
            bool fireGuns = false;
            KeyboardState currentState = Keyboard.GetState();

            // Check for rotation keys.
            if (currentState.IsKeyDown(Keys.A))
                deltaY = 0.0075f;
            else if (currentState.IsKeyDown(Keys.D))
                deltaY = -0.0075f;

            if (currentState.IsKeyDown(Keys.W))
                deltaX = -0.0075f;
            else if (currentState.IsKeyDown(Keys.S))
                deltaX = 0.0075f;

            if (currentState.IsKeyDown(Keys.Space))
                fireGuns = true;

            return new ClientPacket(new Vector2(deltaX, deltaY), fireGuns, id);            
        }

        public int LocalShipLife
        {
            get { if (localShip != null) return localShip.Life; else return 0;  }
        }
    }
}
